#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <assert.h>

#include "db.h"
#include "mail.h"
#include "trace.h"

//!< индексы колонок в таблице
enum
{
	COLUMN_IDX_ID,
	COLUMN_IDX_LINE,
	COLUMN_IDX_TEXT,
	COLUMN_IDX_WAS_SENT,
	COLUMN_COUNT
};

//!< типы колонок в таблице
enum
{
	COLUMN_TYPE_ID       = SQLITE_INTEGER,
	COLUMN_TYPE_LINE     = SQLITE_INTEGER,
	COLUMN_TYPE_TEXT     = SQLITE_TEXT,
	COLUMN_TYPE_WAS_SENT = SQLITE_INTEGER
};

//!< sqlite3 позволяет безопасно работать с базой из разных нитей,
//!< но тем не менее сложная схема блокировок может приводить к тому,
//!< что даже указанный @db_busy_handler иногда не вызывается, а запрос не выполняется.
//!< Поэтому было принято решение обеспечить блокировки посредством мьютекса.
static pthread_mutex_t db_lock = PTHREAD_MUTEX_INITIALIZER;

#define DB_LOCK() \
	do \
	{ \
		TRACE_LOCK("L: %s:%d\n", __FUNCTION__, __LINE__); \
		int prc = 0; \
		if (0 != (prc = pthread_mutex_lock(&db_lock))) \
		{ \
			TRACE_ERR("<%s> pthread_mutex_lock failed with error %d\n", __FUNCTION__, prc); \
		} \
	} while (0)

#define DB_UNLOCK() \
	do \
	{ \
		TRACE_LOCK("U: %s:%d\n", __FUNCTION__, __LINE__); \
		int prc = 0; \
		if (0 != (prc = pthread_mutex_unlock(&db_lock))) \
		{ \
			TRACE_ERR("<%s> pthread_mutex_unlock failed with error %d\n", __FUNCTION__, prc); \
		} \
	} while (0)

#if 0
static int db_busy_handler(void * params, int counter)
{
	TRACE_ERR("<%s> %d\n", __FUNCTION__, counter);
	return 1;
}
#endif

sqlite3 * db_connection_open(const char * path)
{
	TRACE_DBG("<%s>\n", __FUNCTION__);

	if (NULL == path)
	{
		TRACE_ERR("<%s> incoming ptr is NULL\n", __FUNCTION__);
		return NULL;
	}

	char * pragma[] =
	{
		"PRAGMA synchronous = OFF;",
		"PRAGMA cache_size = 4000;",
		"PRAGMA count_changes = NO;",
		"PRAGMA temp_store = MEMORY;",
		"PRAGMA page_size = 4096;",
	};
	const int PRAGMA_COUNT = 5;

	sqlite3 * db = NULL;
	int rc = SQLITE_OK;

	DB_LOCK();
	if (SQLITE_OK != (rc = sqlite3_open_v2(
					path,
					&db,
					/*SQLITE_OPEN_FULLMUTEX |*/ SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
					NULL)))
	{
		TRACE_ERR("<%s> sqlite3_open failed with status %d: %s\n", __FUNCTION__, rc, sqlite3_errmsg(db));
		DB_UNLOCK();
		return NULL;
	}

	size_t idx = 0;
	for (; idx < PRAGMA_COUNT; ++idx)
	{
		if (SQLITE_OK != (rc = sqlite3_exec(db, pragma[idx], NULL, NULL, NULL)))
		{
			TRACE_ERR("<%s> sqlite3_exec failed with status %d: %s\n", __FUNCTION__, rc, sqlite3_errmsg(db));
		}
	}

#if 0
	//!< с использованием внешнего мьютекса не нужно
	if (SQLITE_OK != (rc = sqlite3_busy_timeout(db, 200)))
	{
		TRACE_ERR("<%s> sqlite3_busy_timeout failed with status %d: %s\n", __FUNCTION__, rc, sqlite3_errmsg(db));
	}
	if (SQLITE_OK != (rc = sqlite3_busy_handler(db, db_busy_handler, NULL)))
	{
		TRACE_ERR("<%s> sqlite3_busy_handler failed with status %d: %s\n", __FUNCTION__, rc, sqlite3_errmsg(db));
	}
#endif

	DB_UNLOCK();
	return db;
}

int db_connection_close(sqlite3 * db)
{
	TRACE_DBG("<%s>\n", __FUNCTION__);

	if (NULL == db)
	{
		TRACE_ERR("<%s> incoming ptr is NULL\n", __FUNCTION__);
		return 1;
	}

	int rc = SQLITE_OK;
	DB_LOCK();
	if (SQLITE_OK != (rc = sqlite3_close(db)))
	{
		TRACE_ERR("<%s> sqlite3_close failed with status %d\n", __FUNCTION__, rc);
		DB_UNLOCK();
		return 1;
	}
	DB_UNLOCK();
	return 0;
}

int db_table_create(sqlite3 * db)
{
	TRACE_DBG("<%s>\n", __FUNCTION__);

	if (NULL == db)
	{
		TRACE_ERR("<%s> incoming ptr is NULL\n", __FUNCTION__);
		return 1;
	}
	const char * sql = "CREATE TABLE IF NOT EXISTS messages_table(id INTEGER PRIMARY KEY AUTOINCREMENT, line INTEGER, text VARCHAR(1024), was_sent INTEGER);";

	DB_LOCK();
	int rc = SQLITE_OK;
	if (SQLITE_OK != (rc = sqlite3_exec(db, sql, NULL, NULL, NULL)))
	{
		TRACE_ERR("<%s> sqlite3_exec failed with status %d: %s\n", __FUNCTION__, rc, sqlite3_errmsg(db));
		DB_UNLOCK();
		return 1;
	}
	DB_UNLOCK();

	return 0;
}

int db_entry_find(sqlite3 * db, const char * str, size_t line)
{
	TRACE_DBG("<%s>\n", __FUNCTION__);

	if (NULL == db || NULL == str)
	{
		TRACE_ERR("<%s> incoming ptr is NULL\n", __FUNCTION__);
		return 0;
	}

	char * sql = sqlite3_mprintf("SELECT COUNT(*) FROM messages_table WHERE line = %d and text = '%q'", line, str);
	if (NULL == sql)
	{
		TRACE_ERR("<%s> sqlite3_mprintf failed\n", __FUNCTION__);
		return 0;
	}

	void cleanup() { sqlite3_free(sql); }

	sqlite3_stmt * stmt = NULL;
	int rc = SQLITE_OK;
	DB_LOCK();
	if (SQLITE_OK != (rc = sqlite3_prepare_v2(db, sql, strlen(sql) + 1, &stmt, NULL)))
	{
		TRACE_ERR("<%s> sqlite3_prepare_v2 failed with status %d: %s\n", __FUNCTION__, rc, sqlite3_errmsg(db));
		DB_UNLOCK();
		cleanup();
		return 0;
	}
	size_t rows = 0;
	while (1)
	{
		rc = sqlite3_step(stmt);
		if (SQLITE_ROW == rc)
		{
			rows = sqlite3_column_int(stmt, 0);
		}
		else if (SQLITE_DONE == rc)
		{
			break;
		}
		else
		{
			TRACE_ERR("<%s> sqlite3_step failed with status %d: %s\n", __FUNCTION__, rc, sqlite3_errmsg(db));
			break;
		}
	}
	sqlite3_finalize(stmt);
	DB_UNLOCK();

	//TRACE_DBG("<%s> entry was %sfound\n", __FUNCTION__, 0 == rows ? "not " : "");

	cleanup();
	return rows;
}

int db_entry_insert(sqlite3 * db, const char * str, size_t line)
{
	TRACE_DBG("<%s>\n", __FUNCTION__);

	if (NULL == db || NULL == str)
	{
		TRACE_ERR("<%s> incoming ptr is NULL\n", __FUNCTION__);
		return 1;
	}

	if (0 != db_entry_find(db, str, line))
	{
		return 0;
	}

	char * sql = sqlite3_mprintf("INSERT OR IGNORE INTO messages_table (line, text, was_sent) VALUES (%d, '%q', 0)", line, str);
	if (NULL == sql)
	{
		TRACE_ERR("<%s> sqlite3_mprintf failed\n", __FUNCTION__);
		return 1;
	}

	void cleanup() { sqlite3_free(sql); }

	int rc = SQLITE_OK;
	DB_LOCK();
	if (SQLITE_OK != (rc = sqlite3_exec(db, sql, NULL, NULL, NULL)))
	{
		TRACE_ERR("<%s> sqlite3_exec failed with status %d: %s\n", __FUNCTION__, rc, sqlite3_errmsg(db));
		DB_UNLOCK();
		cleanup();
		return 1;
	}
	DB_UNLOCK();
	cleanup();
	return 0;
}

int db_entry_update(sqlite3 * db, int id)
{
	TRACE_DBG("<%s>\n", __FUNCTION__);

	if (NULL == db)
	{
		TRACE_ERR("<%s> incoming ptr is NULL\n", __FUNCTION__);
		return 1;
	}

	char * sql = sqlite3_mprintf("UPDATE messages_table SET was_sent = 1 WHERE id = %d", id);
	if (NULL == sql)
	{
		TRACE_ERR("<%s> sqlite3_mprintf failed\n", __FUNCTION__);
		return 1;
	}

	void cleanup() { sqlite3_free(sql); }

	int rc = SQLITE_OK;
	DB_LOCK();
	if (SQLITE_OK != (rc = sqlite3_exec(db, sql, NULL, NULL, NULL)))
	{
		TRACE_ERR("<%s> sqlite3_exec failed with status %d: %s\n", __FUNCTION__, rc, sqlite3_errmsg(db));
		DB_UNLOCK();
		cleanup();
		return 1;
	}
	DB_UNLOCK();
	cleanup();
	return 0;
}

int db_new_entries_count(sqlite3 * db)
{
	//TRACE_DBG("<%s>\n", __FUNCTION__);

	if (NULL == db)
	{
		TRACE_ERR("<%s> incoming ptr is NULL\n", __FUNCTION__);
		return 0;
	}

	const char * sql = "SELECT COUNT(*) FROM messages_table WHERE was_sent = 0";

	sqlite3_stmt * stmt = NULL;
	int rc = SQLITE_OK;
	DB_LOCK();
	if (SQLITE_OK != (rc = sqlite3_prepare_v2(db, sql, strlen(sql) + 1, &stmt, NULL)))
	{
		TRACE_ERR("<%s> sqlite3_prepare_v2 failed with status %d: %s\n", __FUNCTION__, rc, sqlite3_errmsg(db));
		DB_UNLOCK();
		return 0;
	}
	int rows = 0;
	while (1)
	{
		rc = sqlite3_step(stmt);
		if (SQLITE_ROW == rc)
		{
			rows = sqlite3_column_int(stmt, 0);
		}
		else if (SQLITE_DONE == rc)
		{
			break;
		}
		else
		{
			TRACE_ERR("<%s> sqlite3_step failed with status %d: %s\n", __FUNCTION__, rc, sqlite3_errmsg(db));
			break;
		}
	}
	sqlite3_finalize(stmt);
	DB_UNLOCK();

	return rows;
}

void db_new_entries_send(sqlite3 * db)
{
	TRACE_DBG("<%s>\n", __FUNCTION__);

	int entry_id[NEW_ENTRIES_AMOUNT];
	memset(entry_id, 0, sizeof(entry_id));
	const char * entry_text[NEW_ENTRIES_AMOUNT + 1];
	memset(entry_text, 0, sizeof(entry_text));

	if (NULL == db)
	{
		TRACE_ERR("<%s> incoming ptr is NULL\n", __FUNCTION__);
		return;
	}

	const char * sql = "SELECT * FROM messages_table WHERE was_sent = 0";

	sqlite3_stmt * stmt = NULL;
	int rc = SQLITE_OK;
	DB_LOCK();
	if (SQLITE_OK != (rc = sqlite3_prepare_v2(db, sql, strlen(sql) + 1, &stmt, NULL)))
	{
		TRACE_ERR("<%s> sqlite3_prepare_v2 failed with status %d: %s\n", __FUNCTION__, rc, sqlite3_errmsg(db));
		DB_UNLOCK();
		return;
	}
	size_t rows = 0;
	while (1)
	{
		rc = sqlite3_step(stmt);
		if (SQLITE_ROW == rc)
		{
			assert(COLUMN_COUNT == sqlite3_column_count(stmt));
			assert(COLUMN_TYPE_ID   == sqlite3_column_type(stmt, COLUMN_IDX_ID));
			assert(COLUMN_TYPE_LINE == sqlite3_column_type(stmt, COLUMN_IDX_LINE));
			assert(COLUMN_TYPE_TEXT == sqlite3_column_type(stmt, COLUMN_IDX_TEXT));

			entry_id[rows] = sqlite3_column_int(stmt, COLUMN_IDX_ID);
			if (0 != sqlite3_column_bytes(stmt, COLUMN_IDX_TEXT))
			{
				entry_text[rows] = strdup((const char *)sqlite3_column_text(stmt, COLUMN_IDX_TEXT));
			}
			else
			{
				assert(0 && "empty string in db !!!");;
			}
			if (NEW_ENTRIES_AMOUNT == ++rows) break;
		}
		else if (SQLITE_DONE == rc)
		{
			break;
		}
		else
		{
			TRACE_ERR("<%s> sqlite3_step failed with status %d: %s\n", __FUNCTION__, rc, sqlite3_errmsg(db));
			break;
		}
	}
	entry_text[rows] = NULL; //!< finalize message body
	sqlite3_finalize(stmt);
	DB_UNLOCK();

	int send_rc = mail_send(entry_text);
	if (0 == send_rc)
	{
		TRACE_DBG("<%s> %d lines was sent !\n", __FUNCTION__, rows);
	}
	else
	{
		TRACE_ERR("<%s> mail_send failed\n", __FUNCTION__);
	}

	size_t idx = 0;
	for (; idx < rows; ++idx)
	{
		if (0 == send_rc)
		{
			db_entry_update(db, entry_id[idx]);
		}
		free((char *)entry_text[idx]);
	}
}

